<?php
/**
 ***********************************************************************************************************************
 * MySQLi Driver
 * @author llq
 ***********************************************************************************************************************
 */
namespace artisan\db;

use mysqli;
use mysqli_result;
use mysqli_sql_exception;
use artisan\log;

/**
 * Class MySQLiDriver
 * @package artisan\db
 */
class MySQLiDriver
{
    /**
     * @var \mysqli mysqli扩展基类
     */
    public $mysqli;

    /**
     * @var bool 当前连接是否处在事务中
     */
    private $inTransaction = false;

    /**
     * @var string 默认地址
     */
    private $host = '127.0.0.1';

    /**
     * @var string 默认账号
     */
    private $user = 'root';

    /**
     * @var string 默认密码
     */
    private $pass = '';

    /**
     * @var int 默认端口
     */
    private $port = 3306;

    /**
     * @var string 默认数据库
     */
    private $db = '';

    /**
     * @var string 默认编码
     */
    private $charset = 'utf8';

    /**
     * @var string 本次执行查询
     */
    private $query;

    /**
     * @var float 本次执行查询耗时
     */
    private $queryTime = 0.00;

    /**
     * @var int 本次执行查询影响条数
     */
    private $affected_rows = 0;

    /**
     * @var array
     */
    private static $sqlConfigs = [
        'SET wait_timeout = 20',
    ];

    /**
     * 初始化本实例 默认初始化时不进行连接
     *
     * @param string $host
     * @param string $user
     * @param string $pass
     * @param string $db
     * @param int $port
     * @param string $charset
     */
    public function __construct($host = '', $user = '', $pass = '', $db = '', $port = 3306, $charset = 'utf8')
    {
        $this->host = empty($host) ? $this->host : (string)$host;
        $this->user = empty($user) ? $this->user : (string)$user;
        $this->pass = empty($pass) ? $this->pass : (string)$pass;
        $this->db = empty($db) ? $this->db : (string)$db;
        $this->port = empty($port) ? $this->port : (int)$port;
        $this->charset = empty($charset) ? $this->charset : (string)$charset;
    }

    /**
     * @return array
     */
    private function preConfig()
    {
        return is_array(self::$sqlConfigs) && self::$sqlConfigs ? array_map(function($sql) {
            $this->query($sql);
        }, self::$sqlConfigs) : [];
    }


    /**
     * 尝试连接
     *
     * @return \mysqli on success or false on failure
     */
    public function connect()
    {
        // 开启错误报告
        mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
        // 尝试创建连接
        try{
            $this->mysqli = new \mysqli(
                $this->host,
                $this->user,
                $this->pass,
                $this->db,
                $this->port
            );

            //开始记录连接数
            DBMonitorUtil::monitorConnectCountsStart($this->mysqli,$this->host,$this->user,$this->db);
            $this->preConfig();
        } catch(\mysqli_sql_exception $e) {

            // 记录错误信息
            //$srv = '(host)' . $this->host . '(user)' . $this->user . '(pass)' . $this->pass . '(db)' . $this->db . 'port' . $this->port;
            $srv = $this->db;
            $this->log('Connect failed ~ [Error]:' . $e->getMessage() . ' ~ [Code]: ' . $e->getCode() . '---' . $srv, 'connect');
            // 追溯触发源头
            $this->log('Debug trace ~ [File]:' . $this->logTrace()['file'] . ' ~ [Line]: ' . $this->logTrace()['line'], 'trace');
            //logDebug(debug_backtrace(false, false), 'trace', 'mysqli');
            return false;
        }
        // 设置初始化字符集
        if(!$this->setCharset($this->charset)) {
            return false;
        }
        return true;
    }

    /**
     * @param $query
     * @param array $pararm
     * @return bool|\mysqli_result
     */
    public function bindQuery($query, array $pararm)
    {
        //全局拦截不需要执行的sql
        if(!DBMonitorUtil::checkSqlIsOk($this->host,$query)){
            return false;
        }

        DBMonitorUtil::setToRedis($query);

        // 重新标记影响行数
        $this->affected_rows = 0;
        $this->queryTime = 0.00;

        // 检查$query合法性
        if(empty($query) || !is_string($query) || empty($pararm)) {
            $this->log('Query check failed ~ Error:[Empty query or pararm given]', 'query_check');
            $this->log('Debug trace ~ File:[' . $this->logTrace()['file'] . '] Line:[' . $this->logTrace()['line'] . ']', 'trace');
            return false;
        }

        // 组织传输参数
        $args = [];
        $arg_fisrt = '';
        foreach($pararm as $val) {
            if(!$type = $this->getDetermineType($val)) {
                $this->log('Pararm check failed ~ Error:[Error bind pararm given]', 'param_check');
                $this->log('Debug trace ~ File:[' . $this->logTrace()['file'] . '] Line:[' . $this->logTrace()['line'] . ']', 'trace');
                return false;
            }
            $arg_fisrt .= $this->getDetermineType($val);
            $args[] = $val;
        }
        if(empty($args) || empty($arg_fisrt)) {
            $this->log('Pararm check failed ~ Error:[Empty bind pararm given]', 'param_check');
            $this->log('Debug trace ~ File:[' . $this->logTrace()['file'] . '] Line:[' . $this->logTrace()['line'] . ']', 'trace');
            return false;
        }
        array_unshift($args, $arg_fisrt);
        // 记录当前执行的query
        $this->query = $this->replacePlaceHolders($query, $args);

        if(!$this->ping()){
            $this->connect();
        }

        try{
            // 准备预编译Query
            if($stmt = $this->mysqli->prepare($query)) {
                // 开始执行时间统计
                $sTime = microtime(true);
                // 传输参数
                call_user_func_array(array($stmt, 'bind_param'), $this->refValues($args));
                // 执行Query
                $ret = $stmt->execute();
                // 记录影响行数
                $this->affected_rows = $stmt->affected_rows;
                // 结束执行时间统计
                $this->queryTime = microtime(true) - $sTime;
                // 如果是检索操作，则返回结果集资源
                if($this->isSelect($query)) {
                    $ret = $this->dynamicBindResults($stmt);
                }
                // 关闭stmt
                $stmt->close();
                // DML操作直接返回执行结果
                return $ret;
            } else {
                $this->log('Prepare query failed ~ Error:[Prepare query failed] Query:[' . $this->query . ']', 'prepare_query');
                $this->log('Debug trace ~ File:[' . $this->logTrace()['file'] . '] Line:[' . $this->logTrace()['line'] . ']', 'trace');
                return false;
            }
        } catch(\mysqli_sql_exception $e) {
            // 唯一索引导致的执行失败不记录
            if(preg_match('/Duplicate\s*entry/', (string)$e->getMessage())) {
                return false;
            }
            $this->log('Query failed ~ Error:[' . $e->getMessage() . '] [Code]:' . $e->getCode().' Query:[' . $this->query . ']', 'query');
            $this->log('Debug trace ~ File:[' . $this->logTrace()['file'] . '] Line:[' . $this->logTrace()['line'] . ']', 'trace');
            return false;
        }
    }

    /**
     * 解析结果集
     * @param \mysqli_stmt $stmt
     * @return array
     */
    protected function dynamicBindResults(\mysqli_stmt $stmt)
    {
        $parameters = array();
        $results = array();
        // See http://php.net/manual/en/mysqli-result.fetch-fields.php
        $mysqlLongType = 252;
        $shouldStoreResult = false;

        $meta = $stmt->result_metadata();

        // if $meta is false yet sqlstate is true, there's no sql error but the query is
        // most likely an update/insert/delete which doesn't produce any results
        if(!$meta && $stmt->sqlstate) {
            return array();
        }

        $row = array();
        while ($field = $meta->fetch_field()) {
            if ($field->type == $mysqlLongType)
                $shouldStoreResult = true;

            $row[$field->name] = null;
            $parameters[] = & $row[$field->name];
        }

        // avoid out of memory bug in php 5.2 and 5.3. Mysqli allocates lot of memory for long*
        // and blob* types. So to avoid out of memory issues store_result is used
        // https://github.com/joshcam/PHP-MySQLi-Database-Class/pull/119
        if ($shouldStoreResult)
            $stmt->store_result();

        call_user_func_array(array($stmt, 'bind_result'), $parameters);

        while ($stmt->fetch()) {
            $x = array();
            foreach ($row as $key => $val)
                $x[$key] = $val;
            array_push($results, $x);
        }
        // stored procedures sometimes can return more then 1 resultset
        if ($this->mysqli->more_results())
            $this->mysqli->next_result();

        return $results;
    }

    /**
     * 判断mysql连接是否已被关闭
     *
     */
    public function hasGoneAway()
    {
        $mysqlGoneAway=false;
        try{
            @$this->mysqli->query('SELECT LAST_INSERT_ID()');
        }catch (\mysqli_sql_exception $e){
            if(2006==($e->getCode())){
                $mysqlGoneAway=true;
            }
        }
        return $mysqlGoneAway?:(2006==mysqli_errno($this->mysqli));
    }


    /**
     * @param $query
     * @return bool|\mysqli_result
     */
    public function query($query)
    {
        //全局拦截不需要执行的sql
        if(!DBMonitorUtil::checkSqlIsOk($this->host,$query)){
            return false;
        }

        DBMonitorUtil::setToRedis($query);

        $this->affected_rows = 0;
        $this->query = $query;
        $this->queryTime = 0.00;

        if(empty($query) || !is_string($query)) {
            $this->log('Query check failed ~ Error:[Empty query or pararm given]', 'query_check');
            $this->log('Debug trace ~ File:[' . $this->logTrace()['file'] . '] Line:[' . $this->logTrace()['line'] . ']', 'trace');
            return false;
        }

        if(!$this->ping()){
            $this->connect();
        }
        try{
            // 开始执行时间统计
            $sTime = microtime(true);
            $ret = $this->mysqli->query($query);
            // 记录影响行数
            $this->affected_rows = $this->mysqli->affected_rows;
            // 结束执行时间统计
            $this->queryTime = microtime(true) - $sTime;
            return $ret;
        } catch(\mysqli_sql_exception $e) {

            // 唯一索引导致的执行失败不记录
            if(preg_match('/Duplicate\s*entry/', (string)$e->getMessage())) {
                return false;
            }
            $this->log('Query failed ~ Error:[' . $e->getMessage() . '] Query:[' . $this->query . ']', 'query');
            $this->log('Debug trace ~ File:[' . $this->logTrace()['file'] . '] Line:[' . $this->logTrace()['line'] . ']', 'trace');
            return false;
        }
    }

    /**
     * 上次执行query
     *
     * @return string
     */
    public function lastQuery()
    {
        return $this->query;
    }

    /**
     * 上次执行queryTime
     *
     * @return float
     */
    public function lastQueryTime()
    {
        return $this->queryTime;
    }

    /**
     * 判断是否是检索操作
     *
     * @param $sql
     * @return bool
     */
    public function isSelect($sql)
    {
        if(preg_match('/^SELECT|SHOW|DESCRIBE|EXPLAIN/', strtoupper(trim(preg_replace('/\s+/', ' ', (string)$sql))))) {
            return true;
        }
        return false;
    }


    /**
     * 解析数据集结果
     *
     * @param null $resource
     * @return array
     */
    public function fetchArray($resource = null)
    {
        if(empty($resource)) {
            return [];
        }
        $result = [];
        while ($row = mysqli_fetch_assoc($resource)) {
            $result[] = $row;
        }
        return $result;
    }

    /**
     * 格式回溯信息
     *
     * @return array
     */
    private function logTrace()
    {
        foreach((array)debug_backtrace(false, false) as $key => $val) {
            if(
                !empty($val['class']) &&
                !empty($val['function']) &&
                ($val['class'] == 'artisan\db\MySQLiAR' || $val['class'] == 'artisan\db\MySQLiAdapterAR') &&
                (in_array($val['function'], ['query', 'bindQuery', 'connect', 'selectDb', 'setCharset']))
            ) {
                return [
                    'file' => !empty($val['file']) ? $val['file'] : '',
                    'line' => !empty($val['line']) ? $val['line'] : '',
                ];
            }
        }
        return [
            'file' => '',
            'line' => ''
        ];
    }

    /**
     * 将结果集解析成数组
     *
     * @param null $resource
     * @return array|bool
     */
    public function fetchToArray($resource = null)
    {
        if(empty($resource)) {
            return false;
        }
        $result = [];
        while($row = mysqli_fetch_assoc($resource)) {
            $result[] = $row;
        }
        return $result;
    }

    /**
     * 重新设置字符集
     * @param $charset
     * @return \mysqli
     */
    public function setCharset($charset)
    {
        $result = $this->mysqli->set_charset($charset);
        if(!$result) {
            $this->log('Set charset failed ~ Error:[' . $this->mysqli->error . ']', 'set_charset');
            $this->log('Debug trace ~ File:[' . $this->logTrace()['file'] . '] Line:[' . $this->logTrace()['line'] . ']', 'trace');
            return false;
        }
        return true;
    }

    /**
     * 重新选择数据
     * @param $db
     * @return \mysqli
     */
    public function selectDb($db)
    {
        $result = $this->mysqli->select_db($db);
        if(!$result) {
            $this->log('Select db failed ~ Error:[' . $this->mysqli->error . ']', 'select_db');
            $this->log('Debug trace ~ File:[' . $this->logTrace()['file'] . '] Line:[' . $this->logTrace()['line'] . ']', 'trace');
            return false;
        }
        return true;
    }

    /**
     * 开始事务
     */
    public function startTransaction()
    {
        $ret = $this->mysqli->autocommit(false);
        $this->inTransaction = true;
        register_shutdown_function(array($this, "transactionCheck"));
        return $ret;
    }

    /**
     * 提交事务
     */
    public function commit()
    {
        $ret = $this->mysqli->commit();
        $this->inTransaction = false;
        $this->mysqli->autocommit(true);
        return $ret;
    }

    /**
     * 回滚事务
     */
    public function rollback()
    {
        $ret = $this->mysqli->rollback();
        $this->inTransaction = false;
        $this->mysqli->autocommit(true);
        return $ret;
    }

    /**
     * 检测并且关闭事务
     */
    public function transactionCheck()
    {
        if(!$this->inTransaction) {
            return;
        }
        $this->rollback();
    }

    /**
     * Insert id
     */
    public function insertId()
    {
        return $this->mysqli->insert_id;
    }

    /**
     * Affected rows
     */
    public function affectedRows()
    {
        return $this->affected_rows;
    }

    /**
     * @return string
     */
    public function getError()
    {
        return $this->mysqli->error;
    }

    /**
     * Escape ill str
     */
    public function escape($str)
    {
        return $this->mysqli->real_escape_string($str);
    }

    /**
     * Client info
     * @return string
     */
    public function getClientInfo()
    {
        return $this->mysqli->get_client_info();
    }

    /**
     * Server version
     * @return string
     */
    public function getServerInfo()
    {
        return $this->mysqli->get_server_info();
    }

    /**
     * Server stat
     * @return string
     */
    public function getServerStat()
    {
        return $this->mysqli->stat();
    }

    /**
     * Connection stat
     * @return bool
     */
    public function getConnectionStat()
    {
        return $this->mysqli->get_connection_stats();
    }


    /**
     * Ping
     */
    public function ping()
    {
        static $timeStamp = null;
        if($timeStamp === null){
            $timeStamp = microtime(true);
            return true;
        }
        if (($tmpTime = microtime(true)) - $timeStamp < 1){
            return true;
        }
        $timeStamp = $tmpTime;
        $ret = false;
        try{
            $ret = $this->mysqli->ping();
        } catch (mysqli_sql_exception $e){
            if ($e->getCode() === 2006) {
                $ret = $this->connect() ?: $this->mysqli->ping();
            }
        }
        return $ret;
    }

    /**
     * Close connection
     */
    public function __destruct()
    {
        $this->close();
    }

    /**
     * 关闭当前连接
     */
    public function close()
    {
        if($this->mysqli) {
            //结束记录连接数
            DBMonitorUtil::monitorConnectCountsEnd($this->mysqli,$this->host,$this->db);

            $isOk=$this->mysqli->close();
            if(!$isOk){
                logDebug('警告:mysql close出错，可能存在多次关闭的情况','msg_cons_err');
            }
            return $isOk;
        }
        return false;
    }

    /**
     * Get item mine
     *
     * @param $item
     * @return string
     */
    private function getDetermineType($item)
    {
        switch (gettype($item)) {
            case 'NULL':
            case 'string':
                return 's';
                break;
            case 'boolean':
            case 'integer':
                return 'i';
                break;
            case 'blob':
                return 'b';
                break;
            case 'double':
                return 'd';
                break;
        }
        return '';
    }

    /**
     * 重置引用
     *
     * @param $arr
     * @return array
     */
    private function refValues($arr)
    {
        if (strnatcmp(phpversion(), '5.3') >= 0) {
            $refs = array();
            foreach ($arr as $key => $value) {
                $refs[$key] = & $arr[$key];
            }
            return $refs;
        }
        return $arr;
    }

    /**
     * 替换SQL中的？
     *
     * @param $str
     * @param $vals
     * @return string
     */
    private function replacePlaceHolders($str, $vals)
    {
        if(empty($vals)) {
            return $str;
        }
        $i = 1;
        $newStr = "";
        while ($pos = strpos ($str, "?")) {
            $val = $vals[$i++];
            if (is_object ($val))
                $val = '[object]';
            if ($val === NULL)
                $val = 'NULL';
            $valStr = is_numeric($val) ? $val : "'". $val . "'";
            $newStr .= substr ($str, 0, $pos) . $valStr;
            $str = substr ($str, $pos + 1);
        }
        $newStr .= $str;
        return $newStr;
    }

    /**
     * 记录日志
     *
     * @param $message
     * @param string $breakpoint
     * @param string $service_name
     */
    private function log($message, $breakpoint = '', $service_name = 'mysqli')
    {
        logError($message, $breakpoint, $service_name);
    }
}